/*******************************************************************************
 * Copyright (c) 2011 Irineu Martins de Lima Moura (imlm2).
 * This file is part of JBeat.
 * 
 *     JBeat is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     JBeat is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Lesser General Public License for more details.
 * 
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with JBeat.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Irineu Martins de Lima Moura (imlm2) - initial API and implementation
 ******************************************************************************/
package br.ufpe.cin.imlm2.jbeat.soot.throwableset.propagation;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import soot.Scene;
import soot.SootClass;
import soot.SootMethod;
import soot.jimple.Stmt;
import soot.jimple.toolkits.callgraph.Edge;
import soot.jimple.toolkits.callgraph.EdgePredicate;
import soot.jimple.toolkits.callgraph.Filter;
import soot.tagkit.Host;
import br.ufpe.cin.imlm2.jbeat.soot.common.CommonOptions;
import br.ufpe.cin.imlm2.jbeat.soot.common.SceneWrapper;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.ThrowableSetTagger;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.tags.PropagatesToTag;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.tags.ThrowableSetTag;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.tags.TrapedSetTag;
import br.ufpe.cin.imlm2.jbeat.soot.traps.TrapManager;
import br.ufpe.cin.imlm2.util.Naming;

public class PropagatesCalculator extends ThrowableSetTagger {

	private boolean withClinits;
	private boolean withSynthetic;
	private boolean interfacesOnly;
	private boolean propagatesTo;
	private EdgePredicate compositePredicate;

	public PropagatesCalculator() {
		super( new PropagationOptions() );
		PropagationOptions ops = (PropagationOptions) this.options;
		this.withClinits = ops.isStaticInitializers();
		this.withSynthetic = ops.isSyntheticEntities();
		this.interfacesOnly = ops.isExceptionInterfacesOnly();
		this.propagatesTo = ops.isPropagatesTo();
	}

	@Override
	protected void tag() {
		super.tag();
		this.propagateExceptions();
	}

	@Override
	protected void executePreTaggingTransformations() {
		super.executePreTaggingTransformations();
		this.withClinits = ((PropagationOptions) this.options).isStaticInitializers();
		this.withSynthetic = ((PropagationOptions) this.options).isSyntheticEntities();
		this.interfacesOnly = ((CommonOptions) this.options).isExceptionInterfacesOnly();
		this.propagatesTo = ((PropagationOptions) this.options).isPropagatesTo();
		CompositeEdgePred cep = new CompositeEdgePred();
		cep.addPredicate( new SourceNotNullStmtOnlyPred() );
		cep.addPredicate( new SourceWithExceptionSetTagsOnlyPred() );
		cep.addPredicate( new TargetNotSyntheticPred() );
		cep.addPredicate( new TargetWithinTargetSetOnlyPred() );
		if(this.interfacesOnly)
			cep.addPredicate(new SrcApplicationOnly());
		this.compositePredicate = cep;
	}

	@Override
	protected void resolveTargetSet() {
		PropagationOptions opts = (PropagationOptions) this.options;
		Collection<SootClass> classes;
		if ( opts.isPropagatedStdLibs() )
			classes = SceneWrapper.getAllClassesAndInterfaces();
		else
			classes = SceneWrapper.getAllApplicationClassesAndInterfaces();

		Collection<SootMethod> methods;
		if ( opts.isOnlyReachables() )
			methods = SceneWrapper.getReachableMethods();
		else
			methods = SceneWrapper.getConcreteMethods();

		for ( SootClass sootClass : classes ) {
			for ( SootMethod sootMethod : sootClass.getMethods() ) {
				if ( methods.contains( sootMethod ) && sootMethod.isConcrete() ) {
					this.getTargetSet().add( sootMethod );
				}
			}
		}

	}

	@Override
	protected void checkTags( SootMethod meth ) {
		super.checkTags( meth );

		if ( !meth.hasTag( TrapedSetTag.TAG_NAME ) ) {
			meth.addTag( new TrapedSetTag() );
		}
		
		if(this.propagatesTo && !meth.hasTag(PropagatesToTag.TAG_NAME)){
			meth.addTag(new PropagatesToTag());
		}
	}

	private void propagateExceptions() {
		for ( Host h : this.getTargetSet() ) {
			SootMethod sootMethod = (SootMethod) h;
			Set<SootClass> exceptions;
			ThrowableSetTag tst = (ThrowableSetTag) sootMethod.getTag( ThrowableSetTag.TAG_NAME );
			if ( sootMethod.getDeclaringClass().isLibraryClass() && this.interfacesOnly ) {
				tst.addAllThrownExceptions( new HashSet<SootClass>( sootMethod.getExceptions() ) );
				exceptions = tst.getAllThrownExceptions();
			} else {
				exceptions = tst.getThrowableSet();
			}
			this.propagateExceptionsFrom( exceptions, sootMethod );
		}
	}

	protected void propagateExceptionsFrom( Set<SootClass> exceptions, SootMethod src ) {
		Set<SootClass> filteredExceptions = filterExceptions( exceptions, src );
		Filter f = new Filter( this.compositePredicate );
		Iterator<Edge> edges = f.wrap( Scene.v().getCallGraph().edgesInto( src ) );

		while ( edges.hasNext() ) {
			Edge next = edges.next();
			SootMethod meth = next.getSrc().method();
			Stmt stm = next.srcStmt();
			
			if(this.propagatesTo){
				PropagatesToTag ptt = (PropagatesToTag) src.getTag(PropagatesToTag.TAG_NAME);
				if(exceptions.size() > 0)
					ptt.addMethod(meth);
			}
			

			ThrowableSetTag throwableSetTag = (ThrowableSetTag) meth.getTag( ThrowableSetTag.TAG_NAME );
			TrapedSetTag trapedSetTag = (TrapedSetTag) meth.getTag( TrapedSetTag.TAG_NAME );
			Set<SootClass> throwableSet = throwableSetTag.getThrowableSet();

			// We want to make sure that all possible sources of an
			// exception are considered.
			Set<SootClass> cacheSet = new HashSet<SootClass>();
			for ( SootClass sootClass : filteredExceptions ) {
				if ( !TrapManager.isExceptionCaughtAt( sootClass, stm, meth.retrieveActiveBody() ) ) {
					cacheSet.add( sootClass );
					if(TrapManager.rethrows(sootClass, stm, meth.retrieveActiveBody())) {
						throwableSetTag.addRethrownException(sootClass, src);
					} else
						throwableSetTag.addPropagatedException(sootClass, src);
				} else {
					trapedSetTag.addTrapedException( sootClass, src );
				}
			}
			if ( !throwableSet.containsAll( filteredExceptions ) ) {
				this.propagateExceptionsFrom( cacheSet, meth );
			}

		}

	}

	private class SourceNotNullStmtOnlyPred implements EdgePredicate {

		@Override
		public boolean want( Edge e ) {
			return e.srcStmt() != null;
		}

	}

	private class SourceWithExceptionSetTagsOnlyPred implements EdgePredicate {

		@Override
		public boolean want( Edge e ) {
			return e.src().hasTag( ThrowableSetTag.TAG_NAME )
					&& e.src().hasTag( TrapedSetTag.TAG_NAME );
		}

	}

	private class CompositeEdgePred implements EdgePredicate {

		private List<EdgePredicate> predicates = new ArrayList<EdgePredicate>();

		public void addPredicate( EdgePredicate pred ) {
			this.predicates.add( pred );
		}

		@Override
		public boolean want( Edge e ) {
			boolean wants = true;
			for ( EdgePredicate pred : this.predicates ) {
				wants = wants && pred.want( e );
			}
			return wants;
		}

	}

	private class TargetWithinTargetSetOnlyPred implements EdgePredicate {

		@Override
		public boolean want( Edge e ) {
			return PropagatesCalculator.this.targetSet.contains( e.getTgt().method() );
		}

	}
	
	private class SrcApplicationOnly implements EdgePredicate {

		@Override
		public boolean want( Edge e ) {
			return e.getSrc().method().getDeclaringClass().isApplicationClass();
		}

	}

	private class TargetNotSyntheticPred implements EdgePredicate {

		@Override
		public boolean want( Edge e ) {
			SootMethod tgt = e.getTgt().method();
			boolean withClinit = PropagatesCalculator.this.withClinits;
			boolean withSynthetic = PropagatesCalculator.this.withSynthetic;
			if ( tgt.getName().equals( "<clinit>" ) && !withClinit )
				return false;
			else if ( Naming.isMethodSynthetic( tgt.getName() ) && !withSynthetic )
				return false;
			else
				return true;
		}

	}

}
